// Copyright (c) 2013, Razvan Petru
// All rights reserved.

// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:

// * Redistributions of source code must retain the above copyright notice, this
//   list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice, this
//   list of conditions and the following disclaimer in the documentation and/or other
//   materials provided with the distribution.
// * The name of the contributors may not be used to endorse or promote products
//   derived from this software without specific prior written permission.

// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

#include "QsLogDestFile.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QTextCodec>
#endif

#include <QDateTime>
#include <QString>
#include <QtGlobal>
#include <QDir>
#include <QDebug>
#include <iostream>

const int QsLogging::SizeRotationStrategy::MaxBackupCount = 10;

QsLogging::RotationStrategy::~RotationStrategy()
{
}

QsLogging::SizeRotationStrategy::SizeRotationStrategy()
    : mCurrentSizeInBytes(0)
    , mMaxSizeInBytes(0)
    , mBackupsCount(0)
{
}

void QsLogging::SizeRotationStrategy::setInitialInfo(QFile &file)
{
    mFileName = file.fileName();
    mCurrentSizeInBytes = file.size();
}

void QsLogging::SizeRotationStrategy::setInitialInfo(const QString &filePath, int fileSize)
{
    mFileName = filePath;
    mCurrentSizeInBytes = fileSize;
}

void QsLogging::SizeRotationStrategy::includeMessageInCalculation(const QString &message)
{
    mCurrentSizeInBytes += message.toUtf8().size();
}

bool QsLogging::SizeRotationStrategy::shouldRotate()
{
    return mCurrentSizeInBytes > mMaxSizeInBytes;
}

// Algorithm assumes backups will be named filename.X, where 1 <= X <= mBackupsCount.
// All X's will be shifted up.
void QsLogging::SizeRotationStrategy::rotate()
{
    if (!mBackupsCount) {
        if (!removeFileAtPath(mFileName)) {
            std::cerr << "QsLog: backup delete failed " << qPrintable(mFileName);
        }
        return;
    }

     // 1. find the last existing backup than can be shifted up
     const QString logNamePattern = mFileName + QString::fromUtf8(".%1");
     int lastExistingBackupIndex = 0;
     for (int i = 1;i <= mBackupsCount;++i) {
         const QString backupFileName = logNamePattern.arg(i);
         if (fileExistsAtPath(backupFileName)) {
             lastExistingBackupIndex = qMin(i, mBackupsCount - 1);
         }
         else {
             break;
         }
     }

     // 2. shift up
     for (int i = lastExistingBackupIndex;i >= 1;--i) {
         const QString oldName = logNamePattern.arg(i);
         const QString newName = logNamePattern.arg(i + 1);
         removeFileAtPath(newName);
         const bool renamed = renameFileFromTo(oldName, newName);
         if (!renamed) {
             std::cerr << "QsLog: could not rename backup " << qPrintable(oldName)
                       << " to " << qPrintable(newName);
         }
     }

     // 3. rename current log file
     const QString newName = logNamePattern.arg(1);
     if (fileExistsAtPath(newName)) {
         removeFileAtPath(newName);
     }
     if (!renameFileFromTo(mFileName, newName)) {
         std::cerr << "QsLog: could not rename log " << qPrintable(mFileName)
                   << " to " << qPrintable(newName);
     }
}

QIODevice::OpenMode QsLogging::SizeRotationStrategy::recommendedOpenModeFlag()
{
    return QIODevice::Append;
}

void QsLogging::SizeRotationStrategy::setMaximumSizeInBytes(qint64 size)
{
    Q_ASSERT(size >= 0);
    mMaxSizeInBytes = size;
}

void QsLogging::SizeRotationStrategy::setBackupCount(int backups)
{
    Q_ASSERT(backups >= 0);
    mBackupsCount = qMin(backups, SizeRotationStrategy::MaxBackupCount);
}

bool QsLogging::SizeRotationStrategy::removeFileAtPath(const QString &path)
{
    return QFile::remove(path);
}

bool QsLogging::SizeRotationStrategy::fileExistsAtPath(const QString &path)
{
    return QFile::exists(path);
}

bool QsLogging::SizeRotationStrategy::renameFileFromTo(const QString &from, const QString &to)
{
    return QFile::rename(from, to);
}

const int QsLogging::NumberRotationStrategy::MaxBackupCount = 10;

QsLogging::NumberRotationStrategy::NumberRotationStrategy()
    : mCurrentNumber(0)
    , mMaxNumber(0)
    , mBackupsCount(0)
{
}

void QsLogging::NumberRotationStrategy::setInitialInfo(QFile &file)
{
    mFileName = file.fileName();
    mCurrentNumber = 0;

    if (!file.isOpen())
    {
        file.open(QIODevice::ReadOnly);
        while (file.readLine().count())
        {
            mCurrentNumber++;
        }
        file.close();
    }
}

void QsLogging::NumberRotationStrategy::includeMessageInCalculation(const QString &message)
{
    Q_UNUSED(message);
    mCurrentNumber++;
}

bool QsLogging::NumberRotationStrategy::shouldRotate()
{
    return mCurrentNumber > mMaxNumber;
}

// Algorithm assumes backups will be named filename.X, where 1 <= X <= mBackupsCount.
// All X's will be shifted up.
void QsLogging::NumberRotationStrategy::rotate()
{
    if (!mBackupsCount) {
        if (!removeFileAtPath(mFileName)) {
            std::cerr << "QsLog: backup delete failed " << qPrintable(mFileName);
        }
        return;
    }

     // 1. find the last existing backup than can be shifted up
     const QString logNamePattern = mFileName + QString::fromUtf8(".%1");
     int lastExistingBackupIndex = 0;
     for (int i = 1;i <= mBackupsCount;++i) {
         const QString backupFileName = logNamePattern.arg(i);
         if (fileExistsAtPath(backupFileName)) {
             lastExistingBackupIndex = qMin(i, mBackupsCount - 1);
         }
         else {
             break;
         }
     }

     // 2. shift up
     for (int i = lastExistingBackupIndex;i >= 1;--i) {
         const QString oldName = logNamePattern.arg(i);
         const QString newName = logNamePattern.arg(i + 1);
         removeFileAtPath(newName);
         const bool renamed = renameFileFromTo(oldName, newName);
         if (!renamed) {
             std::cerr << "QsLog: could not rename backup " << qPrintable(oldName)
                       << " to " << qPrintable(newName);
         }
     }

     // 3. rename current log file
     const QString newName = logNamePattern.arg(1);
     if (fileExistsAtPath(newName)) {
         removeFileAtPath(newName);
     }
     if (!renameFileFromTo(mFileName, newName)) {
         std::cerr << "QsLog: could not rename log " << qPrintable(mFileName)
                   << " to " << qPrintable(newName);
     }
}

QIODevice::OpenMode QsLogging::NumberRotationStrategy::recommendedOpenModeFlag()
{
    return QIODevice::Append;
}

void QsLogging::NumberRotationStrategy::setMaximumNumber(qint64 size)
{
    Q_ASSERT(size >= 0);
    mMaxNumber = size;
}

void QsLogging::NumberRotationStrategy::setBackupCount(int backups)
{
    Q_ASSERT(backups >= 0);
    mBackupsCount = qMin(backups, SizeRotationStrategy::MaxBackupCount);
}

bool QsLogging::NumberRotationStrategy::removeFileAtPath(const QString &path)
{
    return QFile::remove(path);
}

bool QsLogging::NumberRotationStrategy::fileExistsAtPath(const QString &path)
{
    return QFile::exists(path);
}

bool QsLogging::NumberRotationStrategy::renameFileFromTo(const QString &from, const QString &to)
{
    return QFile::rename(from, to);
}

QsLogging::TimeRotationStrategy::TimeRotationStrategy()
    :mDateTime(QDateTime::currentDateTime()),
     mSpaceLimit(256*1024*1024),
     mCurrentSpace(0),
     mRecyclePercent(0.2),
     mpFile( NULL )
{
}


void QsLogging::TimeRotationStrategy::setInitialInfo(QFile &file)
{
    mCurrentSpace = getCurrentSpace();

    mpFile = &file;
}

QString QsLogging::TimeRotationStrategy::getFileName()
{
    QString strFileName = mDateTime.toString( mFormat ) +  mSuffix;
    return mFilePath + QDir::separator() + strFileName;
}

bool QsLogging::TimeRotationStrategy::exceedSpaceLimit()
{
    return (mCurrentSpace>=mSpaceLimit);
}

void QsLogging::TimeRotationStrategy::recycleSpace()
{
    QFileInfoList fileList = getLogFiles();
    qint64 spaceNeedRecycled = mCurrentSpace - mSpaceLimit * (1-mRecyclePercent);
    qint64 spaceRecycled = 0;
    QFileInfo thisFile( mpFile->fileName() );

    for( int i=0; i<fileList.count(); i++ )
    {
        QFileInfo fileInfo = fileList.at(i);

        if( fileInfo.isFile() && ( spaceRecycled < spaceNeedRecycled ) )
        {
            //本文件不回收
            if( thisFile.fileName() != fileInfo.fileName() )
            {
                removeFileAtPath( fileInfo.filePath() );
                spaceRecycled += fileInfo.size();
                mCurrentSpace -= fileInfo.size();
            }
        }
        else
        {
            break;
        }
    }
}

qint64 QsLogging::TimeRotationStrategy::getCurrentSpace()
{
    qint64 space = 0;

    QFileInfoList fileList = getLogFiles();
    for( int i=0; i<fileList.count(); i++ )
    {
        space += fileList.at(i).size();
    }

    return space;
}

QFileInfoList QsLogging::TimeRotationStrategy::getLogFiles()
{
    QDir dir(mFilePath);

    QFileInfoList fileList = dir.entryInfoList( QStringList("*"+mSuffix) );

    return fileList;
}

void QsLogging::TimeRotationStrategy::includeMessageInCalculation(const QString &message)
{
    mCurrentSpace += message.toUtf8().size();
}

bool QsLogging::TimeRotationStrategy::shouldRotate()
{
    //为了和之前的兼容，所以将回收逻辑放在这
    if( exceedSpaceLimit() )
    {
        recycleSpace();
    }

    return ( mDateTime.toString(mFormat) != QDateTime::currentDateTime().toString(mFormat) );
}

// Algorithm assumes no backups but always new file
void QsLogging::TimeRotationStrategy::rotate()
{
    mDateTime = QDateTime::currentDateTime();
    QString strFileName = getFileName();

    if( fileExistsAtPath(strFileName) )
    {
        removeFileAtPath( strFileName );
    }

    mpFile->setFileName( strFileName );
}

QIODevice::OpenMode QsLogging::TimeRotationStrategy::recommendedOpenModeFlag()
{
    return QIODevice::Append;
}

void QsLogging::TimeRotationStrategy::setFilePath( const QString& filePath )
{
    mFilePath = filePath;
}

void QsLogging::TimeRotationStrategy::setTimeFormat( const QString& format )
{
    mFormat = format;
}

void QsLogging::TimeRotationStrategy::setSuffix( const QString& suffix )
{
    mSuffix = suffix;
}
void QsLogging::TimeRotationStrategy::setSpaceLimit( qint64 spaceLimit)
{
    mSpaceLimit = spaceLimit;
}

void QsLogging::TimeRotationStrategy::setRecyclePercent( float fPercent )
{
    mRecyclePercent =  fPercent;
}

bool QsLogging::TimeRotationStrategy::removeFileAtPath(const QString &path)
{
    return QFile::remove(path);
}

bool QsLogging::TimeRotationStrategy::fileExistsAtPath(const QString &path)
{
    return QFile::exists(path);
}

bool QsLogging::TimeRotationStrategy::renameFileFromTo(const QString &from, const QString &to)
{
    return QFile::rename(from, to);
}


const char* const QsLogging::FileDestination::Type = "file";

QsLogging::FileDestination::FileDestination(const QString& filePath, RotationStrategyPtr rotationStrategy)
    : mRotationStrategy(rotationStrategy)
{
    QFileInfo fileInfo(filePath);
    QDir dir;
    dir.mkpath( fileInfo.path() );

    mFile.setFileName(filePath);
    mRotationStrategy->setInitialInfo(mFile);
    if (!mFile.open(QFile::WriteOnly | QFile::Text | mRotationStrategy->recommendedOpenModeFlag())) {
        std::cerr << "QsLog: could not open log file " << qPrintable(filePath);
    }
    mOutputStream.setDevice(&mFile);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    mOutputStream.setCodec(QTextCodec::codecForName("UTF-8"));
#endif

}

void QsLogging::FileDestination::write(const QString& message, Level)
{
    mRotationStrategy->includeMessageInCalculation(message);
    if (mRotationStrategy->shouldRotate())
    {
        mOutputStream.setDevice(NULL);
        mFile.close();
        mRotationStrategy->rotate();
        if (!mFile.open(QFile::WriteOnly | QFile::Text | mRotationStrategy->recommendedOpenModeFlag())) {
            std::cerr << "QsLog: could not reopen log file " << qPrintable(mFile.fileName());
        }
        mRotationStrategy->setInitialInfo(mFile);
        mRotationStrategy->includeMessageInCalculation(message);
        mOutputStream.setDevice(&mFile);
        #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        mOutputStream.setCodec(QTextCodec::codecForName("UTF-8"));
        #endif
    }
    #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    mOutputStream << message<<endl;
    #else
    mOutputStream << message << Qt::endl; // 使用 Qt::endl 替代 endl
    #endif
    mOutputStream.flush();
}

bool QsLogging::FileDestination::isValid()
{
    return mFile.isOpen();
}

QString QsLogging::FileDestination::type() const
{
    return QString::fromLatin1(Type);
}

